(12) class 定义一个类 ES6中通过如下方式定义一个类:
1 2 3 4 5 6 class 类名{ constructor(参数列表){ this.. = ...; } //类的属性与方法 }
然后通过new 类名()的方式创建这个类的实例。其中constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /***ES5写法***/ function Point(x, y){ this.x = x; this.y = y; } /***class写法***/ class Point{ constructor(x, y){ this.x = x; this.y = y; } } let point = new Point(1, 2); console.log(point);//Point { x: 1, y: 2 }
类也有原型,类的原型就是类名后面的{...},也就是说上面的constructor与//类的属性与方法实际上都是定义在类的原型上,然后类的所有实例共享该原型,即实例的原型就是类的原型(即都拥有这些内容,这与ES5一样)。可通过类名.prototype获取类的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /***ES5***/ function Point(x, y){ this.x = x; this.y = y; } console.log(Point.prototype.constructor === Point);//true /***ES6***/ class Point{ constructor(x, y){ this.x = x; this.y = y; } } console.log(Point.prototype.constructor === Point);//true console.log(point.__proto__ === Point.prototype);//true
可见,类名实际上就指向了该类的构造函数
定义类的属性与方法时,不需要加逗号分隔,不需要写成键值对形式。同时,我们在定义类的属性时,一般将属性写在类的最前面,以便清晰地展现该类有哪些属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Point{ _type = 'default';//定义一个属性 constructor(x, y){ this.x = x; this.y = y; } test(){//定义一个方法 console.log('test'); } } let point = new Point(); console.log(point._type);//default point.test();//test
我们可通过[]动态设置类的属性与方法名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let str1 = '_type'; let str2 = 'test'; class Point{ [str1] = 'default'; constructor(x, y){ this.x = x; this.y = y; } [str2](){ console.log('test'); } } let point = new Point(); console.log(point._type);//default point.test();//test
与函数一样,类也可以使用表达式定义
1 2 3 4 5 6 7 const MyClass = class Me { //类的属性与方法 } 或 const MyClass = class { //类的属性与方法 }
上述第一种方式中的Me只能在class内部使用(即类名后的{}中使用)指代类本身。在 class 外部,这个类只能用MyClass引用
采用 class 表达式,可以写出立即执行的 class:
1 2 3 4 5 6 7 8 9 10 11 let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('张三'); person.sayName(); // "张三"
上面代码中,person是一个立即执行的类的实例
上面我们说“//类的属性与方法实际上都是定义在类的原型上”。但这种说法是错的,在此做一个订正:
类的方法确实是定义在类的原型上的。但是类的属性(如前面代码中的_type)不是定义在类的原型上的,而是定义在实例上的。这只是“定义实例属性的一种新写法”。这种新写法详述如下:
实例属性除了定义在constructor()方法里面的this上面,也可以定义在constructor之外的类的最顶层(这时,不需要在实例属性前面加上this)。这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // class A { // constructor(){ // this.x = 2; // } // } /***新写法***/ class A { x = 2; constructor(){ // this.x = 2; } } let a = new A(); console.log(a.x);//2 console.log(A.prototype.x);//undefined
可见类属性并未定义在类的原型上
静态方法 前面说了——类的原型就是实例的原型,因此类中的所有方法为各实例共享。但如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”
1 2 3 4 5 6 7 8 9 10 11 class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
父类的静态方法可以被子类继承
1 2 3 4 5 6 7 8 9 10 class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod() // 'hello'
静态方法中的this关键字指的是类本身,而不是实例
1 2 3 4 5 6 7 8 9 10 11 12 13 class Foo { static bar() { this.baz(); } static baz() { console.log('hello'); } baz() { console.log('world'); } } Foo.bar() // hello
从这个例子还可以看出,静态方法可以与非静态方法重名。特别注意的是:
1 2 3 4 5 6 7 8 9 10 class Foo { static bar() { this.baz(); } baz() { console.log('world'); } } Foo.bar() // TypeError: this.baz is not a function
因为this指向类本身,所以this.baz()表示用类调用方法,而只有静态方法才能被类调用,但是上述baz不是静态方法,所以报错
静态属性 在第一节中我们已经展示了如何在类中定义所有实例的共享属性。但如果在一个属性前,加上static关键字,就表示该属性不会被实例继承,而是直接通过类来调用,这就称为“静态属性”
1 2 3 4 5 6 class Foo { static type = 123; } let foo = new Foo(); console.log(foo.type);//undefined console.log(Foo.type);//123
静态属性也可以被子类继承
1 2 3 4 5 6 7 class A { static type = 123; } class B extends A{ } console.log(B.type);//123
私有属性与方法 以#开头的属性名/方法名表示私有属性/方法。私有属性和方法不能被子类继承、不能由实例使用、不能在类的外部使用、只能在类的内部通过this.#......的形式调用(这儿的this指向的是实例,具体见下面“一些注意点——6”)
1 2 3 4 5 6 7 8 9 10 11 12 class Foo { #type = 123; #test(){ console.log(this.#type); } print(){ this.#test(); } } let foo = new Foo(); foo.print();//123 // console.log(foo.#type);//SyntaxError: Private field '#type' must be declared in an enclosing class
私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法
浏览器对#支持不太好
一些注意点 1、类的内部所有定义的方法,都是不可枚举的(non-enumerable)
2、类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行
3、与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class MyClass { constructor() { // ... } get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } } let inst = new MyClass(); inst.prop = 123; // setter: 123 inst.prop // 'getter'
4、类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式
5、类不存在变量提升(hoist),这一点与 ES5 完全不同
1 2 new Foo(); // ReferenceError class Foo {}
上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部
6、静态方法中的this指向类本身,而非静态方法中的this指向实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Foo { #type = 123; static fun(){ console.log(this === Foo);//true } #test(){ console.log(this === foo);//true console.log(this.#type); } print(){ console.log(this === foo);//true this.#test(); } } let foo = new Foo(); foo.print();//123 Foo.fun();
但是,必须非常小心,一旦单独使用该方法,很可能报错
1 2 3 4 5 6 7 8 9 10 11 12 13 class Logger { printName(name = 'there') { this.print(`Hello ${name}`); } print(text) { console.log(text); } } const logger = new Logger(); const { printName } = logger; printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined),从而导致找不到print方法而报错
至此,类的基本知识已经讨论完毕。下面我们讨论类的继承相关内容
类的继承 继承语法 我们通过语法class Child extends Father表示继承。继承实现的原理是:将子类的原型中的原型设置为父类的原型
1 2 3 4 class A{} class B extends A{} console.log(B.prototype.__proto__ === A.prototype);//true
上述代码的继承原理如下图所示:
可见,如果B的实例要找某个方法或属性便会沿着他的原型链一直找到父类的原型,从而实现了继承。这跟ES5中实现继承的方法是一致的。只不过ES6将整个过程封装为了语法糖
继承中的构造函数(constructor)及构造函数中的super 根据规范,如果子类没有 constructor,那么将生成下面这样的包含super(...)的 constructor:
1 2 3 4 5 class B extends A { constructor(...args) { super(...args); } }
子类构造函数中的super(...)表示调用父类的constructor方法(你可以简单的将其理解为super(...) <==> A.prototype.constructor.call(this),但是这样理解并不准确)。在子类的构造函数中必须先使用super(...)调用父类的构造函数,然后才能使用this,否则创建子类的实例时将报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class A{} class B extends A{ constructor(name){ this.name = name;//ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor } } let b = new B(); /******/ class A{} class B extends A{ constructor(name){ super(); this.name = name; } } let b = new B('德洛丽丝'); console.log(b.name);//德洛丽丝
作为函数调用使用的super(...)只能用在子类的构造函数之中,用在其他地方就会报错
如果我们在子类中也显示定义了子类的constructor,那么这就叫做重写constructor(即重写父类的constructor)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Animal { constructor(name) { this.speed = 0; this.name = name; } } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } } let rabbit = new Rabbit("White Rabbit", 10); console.log(rabbit.name); // White Rabbit console.log(rabbit.earLength); // 10 console.log(rabbit.speed); // 0
重写方法 所谓重写方法是指:按照原型链的搜索过程,子父类中有同名方法时,会执行子类中的方法(而不是父类中的方法)。因此,我们可在子类中覆盖父类中的同名方法,这就叫重写方法
我们除了能在子类中添加子类自己的方法,还能在子类中重写父类中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed = speed; console.log(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; console.log(`${this.name} stands still.`); } } class Rabbit extends Animal { hide() { console.log(`${this.name} hides!`); } stop() { console.log('我是子类的stop'); this.hide(); } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // 我是子类的stop // White Rabbit hides!
上述代码中,子类重写了父类中的stop方法
重写属性 除了重写方法,子类还能重写父类的同名属性
但是,在重写属性时会有一个诡异的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Animal { name = 'animal'; constructor() { alert(this.name); // (*) } } class Rabbit extends Animal { name = 'rabbit'; } new Animal(); // animal new Rabbit(); // animal /******/ class Animal { showName() { // 而不是 this.name = 'animal' alert('animal'); } constructor() { this.showName(); // 而不是 alert(this.name); } } class Rabbit extends Animal { showName() { alert('rabbit'); } } new Animal(); // animal new Rabbit(); // rabbit
在上述第一块代码中的*处,我们理想应该输出rabbit,但是输出的却是父类中的name。但是当使用同名方法时(第二块代码),却能如我们所愿!为什么呢?
实际上,原因在于属性初始化的顺序。类属性是这样初始化的:
对于基类(还未继承任何东西的那种),在构造函数调用前初始化。
对于派生类,在 super() 后立刻初始化
所以,new Rabbit() 调用了 super(),因此它执行了父类构造器,并且(根据派生类规则)只有在此之后,它的类属性才被初始化。在父类构造器被执行的时候,Rabbit 还没有自己的类属性,这就是为什么 Animal 类属性被使用了(会搜索原型链找到父类的name属性)
因此,对于子类而言:在子类的构造函数中的super(...)执行完毕前使用子类中的属性是不可能的
super与this 接下来我们讨论super的相关内容。虽然前面已经有所涉及,但还有很多super的特性并未提及,在此节我们将对此做一个系统梳理
super关键字有三种用法:
通过super.方法名(...)来调用一个父类方法
在子类的构造函数中通过super(...)调用父类的constructor
在普通{}对象的方法中也可以使用super关键字
上述前面两种都与类相关,最后一种与普通对象相关。下面我们分为两类来讨论super
super在普通对象的方法中的使用 此时,super指向它所在对象的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"
如果使用super调用它所在对象的原型中的方法,那么原型方法中的this指向的是super所在对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const proto = { x: 'hello', foo() { console.log(this.x); }, }; const obj = { x: 'world', foo() { super.foo(); } } Object.setPrototypeOf(obj, proto); obj.foo() // "world" 不是hello
需要特别注意的是:super只能用在对象方法的简写形式中(即不是方法名:function(){...}形式定义的方法中,详见“对象的扩展”一章)
super在类中的使用
super作为对象时,在普通方法中,指向父类的原型对象(即class 类名{...}中的{...});在静态方法中,指向父类(即类名所代表的函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class A{ static test1(){ console.log('A的静态方法test1'); } test1(){ console.log('A的公共方法test1'); } } class B extends A{ static test1(){ super.test1(); } test2(){ super.test1(); } } let b = new B(); b.test2();//A的公共方法test1 B.test1();//A的静态方法test1
ES6 规定,在子类普通方法中通过super调用父类的方法时,父类方法内部的this指向当前的子类实例。在子类的静态方法中通过super调用父类的方法时,父类方法内部的this指向当前的子类,而不是子类的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m() // 2 /******/ class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; B.m() // 3
注意,使用super的时候,必须显式指定是作为函数(即super(...)的形式使用super)、还是作为对象使用(即super.属性/方法的形式使用super),否则会报错
1 2 3 4 5 6 7 8 class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } }